﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Notoric.Model.Elements;
using Notoric.Model.Packaging;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Represents a dynamic data collection of references to <see cref="ModelElement"/>s that is backed-up by an XmlElement
    /// and provides notifications when items get added, removed, or when the whole list is refreshed.
    /// </summary>
    /// <remarks>
    /// <para>
    /// For implementation reasons this collection cannot contain <see langword="null"/> items.
    /// When an attempt is made to insert a <see langword="null"/> value to the collection, 
    /// an exception is thrown.
    /// </para>
    /// <para>
    /// The collection is able to recreate itself from the serialized XML representation. For this purpose
    /// the XML element of the item inserted into this collection should contain an attribute called "type"
    /// the value of which is the full name of the item type. Otherwise it is added automatically upon
    /// the insertion.
    /// </para>
    /// <para>
    /// The difference between this collection and the <see cref="ElementCollection{T}"/> is that this collection
    /// does not serialize the content of its elements. Instead it stores only their persistent identifiers.
    /// The deserialization of references is delayed to the first access. In this moment the referenced elements
    /// have to be already deserialized, otherwise a <see cref="SerializationException"/> will occur.
    /// </para>
    /// </remarks>
    /// <typeparam name="T">Type of the collection items (must be a subtype of <see cref="ModelElement"/></typeparam>
    public class ElementReferenceCollection<T> : ICollection<T>, IEnumerable<T>, IList<T>, INotifyCollectionChanged
        where T : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ElementReferenceCollection{T}"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this collection.
        /// </para>
        /// </param>
        /// <param name="collectionElement">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The root element of the collection.<br/>
        /// If the element contains child elements, the collection will attempt 
        /// to recreate its content from these elements.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="owner"/> or <paramref name="collectionElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="SerializationException">
        /// The collection failed to recreate its content.
        /// </exception>
        public ElementReferenceCollection(ModelElement owner, XmlElement collectionElement)
        {
            elements = new ElementCollection<ElementReference<T>>(owner, collectionElement);
            elements.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(elements_CollectionChanged);
        }

        /// <summary>
        /// Creates a new <see cref="ElementReferenceCollection{T}"/> instance that contains
        /// elements copied from a given collection.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this collection.
        /// </para>
        /// </param>
        /// <param name="collectionElement">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The root element of the collection.
        /// </para>
        /// </param>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="IEnumerable{T}"/>
        /// </para>
        /// <para>
        /// The source collection.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="owner"/> or <paramref name="collectionElement"/> or <paramref name="collection"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="collectionElement"/> contains child elements.
        /// </exception>
        public ElementReferenceCollection(ModelElement owner, XmlElement collectionElement, IEnumerable<T> collection)
        {
            elements = new ElementCollection<ElementReference<T>>(owner, collectionElement);

            foreach (T item in collection)
                elements.Add(new ElementReference<T>(owner.PackagePart, owner, item));
        }

        #endregion

        #region ICollection<T> Members

        /// <inheritdoc/>
        public void Add(T item)
        {
            elements.Add(new ElementReference<T>(elements.Owner.PackagePart, elements.Owner, item));
        }

        /// <inheritdoc/>
        public void Clear()
        {
            elements.Clear();
        }

        /// <inheritdoc/>
        public bool Contains(T item)
        {
            return elements.Any(e => e.Reference == item);
        }

        /// <inheritdoc/>
        public void CopyTo(T[] array, int arrayIndex)
        {
            int index = arrayIndex;

            foreach (var reference in elements)
                array[index++] = reference.Reference;
        }

        /// <inheritdoc/>
        public int Count
        {
            get { return elements.Count; }
        }

        /// <inheritdoc/>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <inheritdoc/>
        public bool Remove(T item)
        {
            var index = elements.FindIndex(e => e.Reference == item);
            if (index < 0)
                return false;

            elements.RemoveAt(index);
            return true;
        }

        /// <inheritdoc/>
        public IEnumerator<T> GetEnumerator()
        {
            var elems = from element in elements select (T)element;

            return elems.GetEnumerator();
        }

        /// <inheritdoc/>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region IList<T> Members

        /// <inheritdoc/>
        public int IndexOf(T item)
        {
            return elements.FindIndex(e => e.Reference == item);
        }

        /// <inheritdoc/>
        public void Insert(int index, T item)
        {
            elements.Insert(index, new ElementReference<T>(elements.Owner.PackagePart, elements.Owner, item));
        }

        /// <inheritdoc/>
        public void RemoveAt(int index)
        {
            elements.RemoveAt(index);
        }

        /// <inheritdoc/>
        public T this[int index]
        {
            get
            {
                return elements[index].Reference;
            }
            set
            {
                elements[index] = new ElementReference<T>(elements.Owner.PackagePart, elements.Owner, value);
            }
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <inheritdoc/>
        public event EventHandler<NotifyCollectionChangedEventArgs> CollectionChanged;

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the elements collection has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void elements_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
            {
                NotifyCollectionChangedEventArgs args = null; 
                
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        args = new NotifyCollectionChangedEventArgs(
                            e.Action,
                            ((ElementReference<T>)e.NewItems[0]).Reference,
                            e.NewStartingIndex
                            );
                        break;
                    case NotifyCollectionChangedAction.Move:
                        args = new NotifyCollectionChangedEventArgs(
                            e.Action,
                            ((ElementReference<T>)e.OldItems[0]).Reference,
                            e.NewStartingIndex,
                            e.OldStartingIndex
                            );
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        args = new NotifyCollectionChangedEventArgs(
                            e.Action,
                            ((ElementReference<T>)e.OldItems[0]).Reference,
                            e.OldStartingIndex
                            );
                        break;
                    case NotifyCollectionChangedAction.Replace:
                        args = new NotifyCollectionChangedEventArgs(
                            e.Action,
                            new List<T>(from item in e.NewItems.Cast<ElementReference<T>>() select (T)item),
                            new List<T>(from item in e.OldItems.Cast<ElementReference<T>>() select (T)item),
                            e.NewStartingIndex
                            );
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        args = new NotifyCollectionChangedEventArgs();
                        break;
                }
                
                CollectionChanged(this, args);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Moves the item at the specified index to a new location in the collection.
        /// </summary>
        /// <param name="oldIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the location of the item to be moved.
        /// </para>
        /// </param>
        /// <param name="newIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the new location of the item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Either of the indices are not valid.
        /// </exception>
        public void Move(int oldIndex, int newIndex)
        {
            elements.Move(oldIndex, newIndex);
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the collection of element references.
        /// </summary>
        private ElementCollection<ElementReference<T>> elements;

        #endregion
    }
}
